Skip to content

feat: Logs table panel - PoC#1752

Draft
gtk-grafana wants to merge 8 commits intomainfrom
gtk-grafana/logs-table-panel
Draft

feat: Logs table panel - PoC#1752
gtk-grafana wants to merge 8 commits intomainfrom
gtk-grafana/logs-table-panel

Conversation

@gtk-grafana
Copy link
Copy Markdown
Contributor

No description provided.

@gtk-grafana gtk-grafana requested a review from a team as a code owner February 18, 2026 03:34
@gtk-grafana gtk-grafana marked this pull request as draft February 18, 2026 03:34
@matyax matyax self-assigned this Mar 25, 2026
@matyax matyax force-pushed the gtk-grafana/logs-table-panel branch from 5200c68 to b193c7a Compare March 30, 2026 13:39
@matyax matyax force-pushed the gtk-grafana/logs-table-panel branch from f6e2a6c to 0842c19 Compare April 13, 2026 09:29
@matyax matyax marked this pull request as ready for review April 15, 2026 15:19
Copilot AI review requested due to automatic review settings April 15, 2026 15:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a feature-flagged “next-gen” logs table panel PoC, including persistence of table column widths and a new wrapText option stored in localStorage.

Changes:

  • Introduces LogsTablePanelScene that renders the logstable viz panel and hooks it into LogsListScene behind logsTablePanelNG.
  • Persists/restores table column widths via new store helpers and a new services/logsTable helper module.
  • Extends feature flags + localStorage options to support logsTablePanelNG and wrapText.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/services/store.ts Adds localStorage persistence for table column widths; extends boolean log options with wrapText.
src/services/logsTable.ts New helper for extracting/saving width overrides and reapplying them to the table builder.
src/featureFlags/openFeature.ts Adds logsTablePanelNG flag and config fallback wiring (with a duplicated fallback branch).
src/Components/ServiceScene/LogsTablePanelScene.tsx New Scene implementation for logstable panel; URL syncing/displayed field interactions and header actions wiring.
src/Components/ServiceScene/LogsListScene.tsx Gates the new table panel behind logsTablePanelNG.
src/Components/ServiceScene/LogOptionsScene.tsx Alters how the scene finds its “logs panel” parent (now using an any cast to this.parent).
project-words.txt Adds logstable to dictionary.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +234 to +244
const searchParams = new URLSearchParams(locationService.getLocation().search);
let urlColumns: string[] | null = [];
try {
urlColumns = unknownToStrings(JSON.parse(decodeURIComponent(searchParams.get('urlColumns') ?? '')));
// If body or line is in the url columns, show the line state controls
if (urlColumns.includes(DATAPLANE_BODY_NAME_LEGACY) || urlColumns.includes(DATAPLANE_LINE_NAME)) {
this.setState({ isDisabledLineState: true });
}
} catch (e) {
console.error(e);
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same parsing issue as above: defaulting urlColumns to '' makes JSON.parse('') throw whenever the param is absent. Use a safe JSON default ('[]') or skip parsing when the param is null/empty; also avoid console.error here since logger is already available in this file.

Copilot uses AI. Check for mistakes.
displayedFields: newDisplayedFields,
});
// sync LocalStorage displayedFields for Go to explore
setDisplayedFieldsInStorage(this, parentModel.state.displayedFields);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After parentModel.setState({ displayedFields: newDisplayedFields }), local storage is updated using parentModel.state.displayedFields, which may still be the previous value (state updates are async). Persist newDisplayedFields directly (or update storage in a state subscription) to ensure the stored displayed fields match the new selection.

Suggested change
setDisplayedFieldsInStorage(this, parentModel.state.displayedFields);
setDisplayedFieldsInStorage(this, newDisplayedFields);

Copilot uses AI. Check for mistakes.
Comment thread src/services/store.ts
Comment on lines +288 to +296
? stored.filter(
(columnWidth: unknown): columnWidth is TableColumnWidth =>
typeof columnWidth === 'object' &&
columnWidth !== null &&
'field' in columnWidth &&
'width' in columnWidth &&
typeof columnWidth.width === 'number' &&
typeof columnWidth.field === 'string'
)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard in stored.filter(...) accesses columnWidth.width / columnWidth.field even though columnWidth is typed as unknown. In TypeScript this doesn’t narrow to a property-accessible shape, so this is likely a compile-time error. Consider first narrowing to a record (e.g., via an isRecord helper) or casting to Record<string, unknown> after the typeof === 'object' check before reading properties; then narrow width/field from that record.

Suggested change
? stored.filter(
(columnWidth: unknown): columnWidth is TableColumnWidth =>
typeof columnWidth === 'object' &&
columnWidth !== null &&
'field' in columnWidth &&
'width' in columnWidth &&
typeof columnWidth.width === 'number' &&
typeof columnWidth.field === 'string'
)
? stored.filter((columnWidth: unknown): columnWidth is TableColumnWidth => {
if (typeof columnWidth !== 'object' || columnWidth === null) {
return false;
}
const record = columnWidth as Record<string, unknown>;
return (
'field' in record &&
'width' in record &&
typeof record.width === 'number' &&
typeof record.field === 'string'
);
})

Copilot uses AI. Check for mistakes.
Comment thread src/services/logsTable.ts
Comment on lines +12 to +22
const widthOverrides = config.overrides
.filter((override) => override.matcher.id === 'byName')
.filter((override) => override.properties.some((property) => property.id === 'custom.width' && property.value > 0))
.map((override) => {
const field = override.matcher.options;
const width = override.properties.find((property) => property.id === 'custom.width' && property.value > 0)?.value;
return {
field,
width,
};
});
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

widthOverrides can end up storing entries where width is undefined (the find(...)? is still optional) and/or field isn’t a string (matcher options isn’t validated). This will either fail the TableColumnWidth contract or silently drop widths later when reloading. Prefer extracting the matching custom.width property once, assert typeof value === 'number' and value > 0, and only persist { field: string, width: number } entries.

Suggested change
const widthOverrides = config.overrides
.filter((override) => override.matcher.id === 'byName')
.filter((override) => override.properties.some((property) => property.id === 'custom.width' && property.value > 0))
.map((override) => {
const field = override.matcher.options;
const width = override.properties.find((property) => property.id === 'custom.width' && property.value > 0)?.value;
return {
field,
width,
};
});
const widthOverrides = config.overrides.flatMap((override) => {
if (override.matcher.id !== 'byName') {
return [];
}
const field = override.matcher.options;
const width = override.properties.find((property) => property.id === 'custom.width')?.value;
if (typeof field !== 'string' || typeof width !== 'number' || width <= 0) {
return [];
}
return [
{
field,
width,
},
];
});

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +149
panel.setState({
showMenuAlways: true,
menu: new PanelMenu({}),
headerActions: new LogOptionsScene({
onChangeVisualizationType: parentScene.setVisualizationType,
visualizationType: parentScene.state.visualizationType,
}),
});
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headerActions wires in LogOptionsScene, but LogOptionsScene reads sortOrder/wrapLogMessage from getLogsPanelScene() and mutates that scene’s state. In this context the parent is a VizPanel (not a LogsPanelScene), so this is likely to throw or mutate the wrong object at runtime. Consider providing table-appropriate header actions (or adapting LogOptionsScene to work with both logs and table panels via an explicit interface).

Copilot uses AI. Check for mistakes.
Comment thread src/services/logsTable.ts
Comment on lines +11 to +36
export function storeTableFieldConfig(config: FieldConfigSource, sceneRef: SceneObject) {
const widthOverrides = config.overrides
.filter((override) => override.matcher.id === 'byName')
.filter((override) => override.properties.some((property) => property.id === 'custom.width' && property.value > 0))
.map((override) => {
const field = override.matcher.options;
const width = override.properties.find((property) => property.id === 'custom.width' && property.value > 0)?.value;
return {
field,
width,
};
});
saveTableColumnWidths(sceneRef, widthOverrides);
}

export function setTableFieldOverrides(
builder: FieldConfigOverridesBuilder<LogsTableFieldConfig>,
sceneRef: SceneObject
) {
const columnWidths = getTableColumnWidths(sceneRef);

columnWidths.forEach((columnWidth) => {
builder
.matchFieldsWithName(columnWidth.field)
.overrideCustomFieldConfig<LogsTableFieldConfig, 'width'>('width', columnWidth.width);
});
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New persistence behavior (extracting custom.width overrides and reapplying them via setTableFieldOverrides) isn’t covered by tests. Adding unit tests around storeTableFieldConfig/setTableFieldOverrides (including invalid/partial overrides and corrupted localStorage payloads) would help prevent regressions in column sizing persistence.

Copilot uses AI. Check for mistakes.
Comment on lines +305 to +307
if (flagName === 'exploreLogsShardSplitting') {
return config.featureToggles.exploreLogsShardSplitting;
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getConfigToggleFallback checks flagName === 'exploreLogsShardSplitting' twice. The second branch is unreachable and should be removed to avoid confusion about intended fallback behavior.

Suggested change
if (flagName === 'exploreLogsShardSplitting') {
return config.featureToggles.exploreLogsShardSplitting;
}

Copilot uses AI. Check for mistakes.
Comment on lines 62 to 66
getLogsPanelScene = () => {
return sceneGraph.getAncestor(this, LogsPanelScene);
//@todo fix
return this.parent as any;
// return sceneGraph.getAncestor(this, LogsPanelScene);
};
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getLogsPanelScene currently returns this.parent as any. This is brittle and can easily break at runtime when LogOptionsScene is mounted under something other than LogsPanelScene (e.g., a VizPanel headerActions). Prefer resolving the correct scene via sceneGraph.getAncestor(...) (or pass an explicit reference/callbacks in state) so the renderer can reliably read/update sortOrder, wrapLogMessage, etc. without any casts.

Copilot uses AI. Check for mistakes.
Comment on lines +216 to +230
const searchParams = new URLSearchParams(locationService.getLocation().search);
// Check URL columns for body parameter and update isDisabledLineState accordingly
let urlColumns: string[] | null = [];
try {
urlColumns = unknownToStrings(JSON.parse(decodeURIComponent(searchParams.get('urlColumns') ?? '')));

// If body or line is in the url columns, show the line state controls
if (urlColumns.includes(DATAPLANE_BODY_NAME_LEGACY) || urlColumns.includes(DATAPLANE_LINE_NAME)) {
this.setState({ isDisabledLineState: true });
} else {
this.setState({ isDisabledLineState: false });
}
} catch (e) {
console.error('Error parsing urlColumns:', e);
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When urlColumns is missing/empty, JSON.parse(decodeURIComponent(searchParams.get('urlColumns') ?? '')) will always throw (JSON.parse('') is invalid), causing noisy errors on every navigation. Use a safe default like '[]' (or guard on null/empty) before parsing, and prefer the shared logger instead of console.error for consistency/telemetry.

Copilot uses AI. Check for mistakes.
@matyax matyax marked this pull request as draft April 15, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants